Skip to content

Add cross-SDK request validator test vectors and fix URL normalization#39

Merged
Fivell merged 10 commits intomainfrom
test/request-validator-vectors
Mar 12, 2026
Merged

Add cross-SDK request validator test vectors and fix URL normalization#39
Fivell merged 10 commits intomainfrom
test/request-validator-vectors

Conversation

@Fivell
Copy link
Member

@Fivell Fivell commented Mar 10, 2026

Summary

  • Add 5 cross-SDK validation test vectors + 20 URL normalization test vectors
  • Use timing-safe comparison (MessageDigest.isEqual)

Test plan

  • All 25 tests pass

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves webhook signature validation by making URL normalization accept inputs without an explicit scheme (by prepending http://) and adds a parameterized test suite of cross-SDK URL normalization vectors to lock in expected signatures.

Changes:

  • Prepend http:// when the input URL lacks a :// scheme delimiter.
  • Add 15 URL normalization/signature “golden” vectors via a JUnit 5 parameterized test.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/main/java/com/didww/sdk/callback/RequestValidator.java Adjusts URL normalization to handle no-scheme URLs by prepending http://.
src/test/java/com/didww/sdk/callback/RequestValidatorTest.java Adds parameterized test vectors to validate URL normalization + signature results across common URL variants.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 38 to 43
if (!url.matches("^[a-zA-Z]+://.*")) {
url = "http://" + url;
}
URI uri = URI.create(url);
String scheme = uri.getScheme();
String userInfo = uri.getUserInfo() != null ? uri.getUserInfo() + "@" : "";
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normalizeUrl uses uri.getScheme() later to pick default ports and also includes it in the normalized URL. URI preserves the original scheme casing (e.g., HTTPS://... -> scheme "HTTPS"), but schemes are case-insensitive; this can cause signature mismatches and wrong default-port selection. Consider lowercasing the parsed scheme (and using the lowercased value consistently for both port selection and the returned normalized URL).

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +40
if (!url.matches("^[a-zA-Z]+://.*")) {
url = "http://" + url;
}
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String#matches recompiles the regex on every call. Since normalizeUrl runs for every validation, consider avoiding regex here (e.g., a small character scan / indexOf("://")) or using a precompiled Pattern constant. If you keep regex, [A-Za-z][A-Za-z0-9+.-]*:// is a more RFC-compatible scheme pattern than ^[a-zA-Z]+://.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}
URI uri = URI.create(url);
String scheme = uri.getScheme();
String scheme = uri.getScheme().toLowerCase();
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uri.getScheme().toLowerCase() is locale-sensitive and can produce incorrect results under certain default locales (e.g., Turkish), which would break signature validation. Use locale-independent lowercasing (e.g., toLowerCase(Locale.ROOT)) when normalizing the scheme.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +75
static Stream<Object[]> urlNormalizationVectors() {
return Stream.of(
new Object[]{"http://foo.com/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"},
new Object[]{"http://foo.com:80/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"},
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method source currently returns Stream<Object[]>, which is less type-safe and harder to read/maintain than using JUnit 5's Stream<Arguments> (or a dedicated record/class). Switching to Arguments.of(url, expectedSignature) will improve clarity and avoids accidental argument-shape mistakes.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +96 to +101
RequestValidator validator = new RequestValidator("SOMEAPIKEY");
Map<String, String> payload = new LinkedHashMap<>();
payload.put("id", "1dd7a68b-e235-402b-8912-fe73ee14243a");
payload.put("status", "completed");
payload.put("type", "orders");

Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The payload setup here is duplicated across multiple tests in this class (same keys/values with only ordering differences). Extracting a small helper (e.g., a method that returns the common payload map) would reduce repetition and make it easier to update the test data consistently if the signature inputs change again.

Copilot uses AI. Check for mistakes.
Comment on lines 42 to 48
if (!SCHEME_PATTERN.matcher(url).find()) {
url = "http://" + url;
}
URI uri = URI.create(url);
String scheme = uri.getScheme();
String scheme = uri.getScheme().toLowerCase(Locale.ROOT);
String userInfo = uri.getUserInfo() != null ? uri.getUserInfo() + "@" : "";
String host = uri.getHost();
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normalizeUrl prepends http:// when no scheme is detected, but some inputs (e.g., /path, //foo.com/bar, or other non-host relative forms) will parse with uri.getHost() == null and the method will return a normalized URL containing the literal string null as the host. Consider explicitly validating that uri.getHost() is non-null/non-empty (and optionally handling scheme-relative URLs starting with //) and returning ""/failing validation instead of producing a canonical form with a null host.

Copilot uses AI. Check for mistakes.
@Fivell Fivell force-pushed the test/request-validator-vectors branch from a7e30cc to f3c89f0 Compare March 11, 2026 16:19
@Fivell Fivell requested a review from Copilot March 11, 2026 16:31
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +65 to +81
static Stream<Arguments> urlNormalizationVectors() {
return Stream.of(
Arguments.of("http://foo.com/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"),
Arguments.of("http://foo.com:80/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"),
Arguments.of("http://foo.com:443/bar", "904eaa65c0759afac0e4d8912de424e2dfb96ea1"),
Arguments.of("http://foo.com:8182/bar", "eb8fcfb3d7ed4b4c2265d73cf93c31ba614384d1"),
Arguments.of("http://foo.com/bar?baz=boo", "78b00717a86ce9df06abf45ff818aa94537e1729"),
Arguments.of("http://user:pass@foo.com/bar", "88615a11a78c021c1da2e1e0bfb8cc165170afc5"),
Arguments.of("http://foo.com/bar#test", "b1c4391fcdab7c0521bb5b9eb4f41f08529b8418"),
Arguments.of("https://foo.com/bar", "f26a771c302319a7094accbe2989bad67fff2928"),
Arguments.of("https://foo.com:443/bar", "f26a771c302319a7094accbe2989bad67fff2928"),
Arguments.of("https://foo.com:80/bar", "bd45af5253b72f6383c6af7dc75250f12b73a4e1"),
Arguments.of("https://foo.com:8384/bar", "9c9fec4b7ebd6e1c461cb8e4ffe4f2987a19a5d3"),
Arguments.of("https://foo.com/bar?qwe=asd", "4a0e98ddf286acadd1d5be1b0ed85a4e541c3137"),
Arguments.of("https://qwe:asd@foo.com/bar", "7a8cd4a6c349910dfecaf9807e56a63787250bbd"),
Arguments.of("https://foo.com/bar#baz", "5024919770ea5ca2e3ccc07cb940323d79819508")
);
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description mentions 'Added 15 hardcoded cross-SDK test vectors', but this method currently defines 14 Arguments.of(...) entries. Either add the missing vector or adjust the PR description so it matches the code.

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +80
static Stream<Arguments> urlNormalizationVectors() {
return Stream.of(
Arguments.of("http://foo.com/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"),
Arguments.of("http://foo.com:80/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"),
Arguments.of("http://foo.com:443/bar", "904eaa65c0759afac0e4d8912de424e2dfb96ea1"),
Arguments.of("http://foo.com:8182/bar", "eb8fcfb3d7ed4b4c2265d73cf93c31ba614384d1"),
Arguments.of("http://foo.com/bar?baz=boo", "78b00717a86ce9df06abf45ff818aa94537e1729"),
Arguments.of("http://user:pass@foo.com/bar", "88615a11a78c021c1da2e1e0bfb8cc165170afc5"),
Arguments.of("http://foo.com/bar#test", "b1c4391fcdab7c0521bb5b9eb4f41f08529b8418"),
Arguments.of("https://foo.com/bar", "f26a771c302319a7094accbe2989bad67fff2928"),
Arguments.of("https://foo.com:443/bar", "f26a771c302319a7094accbe2989bad67fff2928"),
Arguments.of("https://foo.com:80/bar", "bd45af5253b72f6383c6af7dc75250f12b73a4e1"),
Arguments.of("https://foo.com:8384/bar", "9c9fec4b7ebd6e1c461cb8e4ffe4f2987a19a5d3"),
Arguments.of("https://foo.com/bar?qwe=asd", "4a0e98ddf286acadd1d5be1b0ed85a4e541c3137"),
Arguments.of("https://qwe:asd@foo.com/bar", "7a8cd4a6c349910dfecaf9807e56a63787250bbd"),
Arguments.of("https://foo.com/bar#baz", "5024919770ea5ca2e3ccc07cb940323d79819508")
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the PR claims a fix for no-schema URL handling (prepending http://), the normalization vectors should include at least one no-scheme case (e.g., foo.com/bar) to lock in that behavior. Without it, the reported bug fix isn't covered by tests.

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +74
sb.append(String.format("%02x", b)); // NOSONAR
}
return sb.toString();
} catch (Exception e) {
throw new RuntimeException("Failed to compute HMAC-SHA1", e);
throw new RuntimeException("Failed to compute HMAC-SHA1", e); // NOSONAR
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using // NOSONAR here suppresses all static-analysis findings on the line, including future ones. Prefer addressing the underlying warning (e.g., avoid String.format in the loop) or use a more targeted suppression so real issues in this code path aren't hidden.

Copilot uses AI. Check for mistakes.
try {
URI uri = URI.create(url);
String scheme = uri.getScheme();
String scheme = uri.getScheme().toLowerCase(Locale.ROOT);
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uri.getScheme() can be null for no-scheme inputs like foo.com/bar. Calling .toLowerCase(Locale.ROOT) will throw NPE and normalizeUrl will fall back to returning an empty string, which breaks the stated 'prepend http://' behavior and changes signature computation. Handle a null scheme explicitly (e.g., treat as http and re-parse with http:// prefix) before lowercasing.

Suggested change
String scheme = uri.getScheme().toLowerCase(Locale.ROOT);
String scheme = uri.getScheme();
if (scheme == null) {
uri = URI.create("http://" + url);
scheme = uri.getScheme();
}
scheme = scheme.toLowerCase(Locale.ROOT);

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

StringBuilder sb = new StringBuilder();
for (byte b : rawHmac) {
sb.append(String.format("%02x", b));
sb.append(String.format("%02x", b)); // NOSONAR
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid using // NOSONAR to suppress the locale warning here; instead pass an explicit locale to String.format (e.g., Locale.ROOT). This keeps the intent clear and removes the need for suppression while still producing stable lowercase hex output across environments.

Suggested change
sb.append(String.format("%02x", b)); // NOSONAR
sb.append(String.format(Locale.ROOT, "%02x", b));

Copilot uses AI. Check for mistakes.
Comment on lines 73 to 75
} catch (Exception e) {
throw new RuntimeException("Failed to compute HMAC-SHA1", e);
throw new RuntimeException("Failed to compute HMAC-SHA1", e); // NOSONAR
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, the // NOSONAR suppression on the thrown exception makes it harder to understand what is being suppressed and why. If Sonar is flagging the exception type/message, it’s better to address the underlying finding (e.g., narrowing the caught exceptions or using a more specific exception type) rather than suppressing it inline.

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +82
static Stream<Arguments> urlNormalizationVectors() {
return Stream.of(
Arguments.of("http://foo.com/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"),
Arguments.of("http://foo.com:80/bar", "4d1ce2be656d20d064183bec2ab98a2ff3981f73"),
Arguments.of("http://foo.com:443/bar", "904eaa65c0759afac0e4d8912de424e2dfb96ea1"),
Arguments.of("http://foo.com:8182/bar", "eb8fcfb3d7ed4b4c2265d73cf93c31ba614384d1"),
Arguments.of("http://foo.com/bar?baz=boo", "78b00717a86ce9df06abf45ff818aa94537e1729"),
Arguments.of("http://user:pass@foo.com/bar", "88615a11a78c021c1da2e1e0bfb8cc165170afc5"),
Arguments.of("http://foo.com/bar#test", "b1c4391fcdab7c0521bb5b9eb4f41f08529b8418"),
Arguments.of("https://foo.com/bar", "f26a771c302319a7094accbe2989bad67fff2928"),
Arguments.of("https://foo.com:443/bar", "f26a771c302319a7094accbe2989bad67fff2928"),
Arguments.of("https://foo.com:80/bar", "bd45af5253b72f6383c6af7dc75250f12b73a4e1"),
Arguments.of("https://foo.com:8384/bar", "9c9fec4b7ebd6e1c461cb8e4ffe4f2987a19a5d3"),
Arguments.of("https://foo.com/bar?qwe=asd", "4a0e98ddf286acadd1d5be1b0ed85a4e541c3137"),
Arguments.of("https://qwe:asd@foo.com/bar", "7a8cd4a6c349910dfecaf9807e56a63787250bbd"),
Arguments.of("https://foo.com/bar#baz", "5024919770ea5ca2e3ccc07cb940323d79819508")
);
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions “15 hardcoded … test vectors”, but urlNormalizationVectors() currently contains 14 entries. Either add the missing vector or update the PR description so it matches what’s being tested.

Copilot uses AI. Check for mistakes.
@Fivell Fivell changed the title Add URL normalization test vectors and fix no-schema URL handling Add cross-SDK request validator test vectors and fix URL normalization Mar 11, 2026
@Fivell Fivell requested a review from Copilot March 11, 2026 23:31
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Arguments.of("http://foo.com/bar?baz=boo", "78b00717a86ce9df06abf45ff818aa94537e1729"),
Arguments.of("http://user:pass@foo.com/bar", "88615a11a78c021c1da2e1e0bfb8cc165170afc5"),
Arguments.of("http://foo.com/bar#test", "b1c4391fcdab7c0521bb5b9eb4f41f08529b8418"),
Arguments.of("https://foo.com/bar", "f26a771c302319a7094accbe2989bad67fff2928"),
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The URL normalization change lowercases the scheme, but the new test vectors don't exercise mixed/upper-case schemes (e.g., "HTTP://foo.com/bar"). Adding a vector that differs only by scheme case would ensure the behavior stays consistent and guards against regressions in normalizeUrl(...).

Suggested change
Arguments.of("https://foo.com/bar", "f26a771c302319a7094accbe2989bad67fff2928"),
Arguments.of("https://foo.com/bar", "f26a771c302319a7094accbe2989bad67fff2928"),
Arguments.of("HTTPS://foo.com/bar", "f26a771c302319a7094accbe2989bad67fff2928"),

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Fivell added 7 commits March 12, 2026 01:39
…alidator

- Lowercase the URI scheme after parsing to handle mixed-case schemes correctly
- Replace url.matches() with a precompiled static Pattern to avoid recompiling the regex on every call
- Use toLowerCase(Locale.ROOT) for scheme normalization to avoid
  locale-dependent behavior (e.g. Turkish locale)
- Restore SCHEME_PATTERN and no-scheme URL handling
- Change Stream<Object[]> to Stream<Arguments> with Arguments.of()
  for type-safe JUnit 5 parameterized tests
- Restore URL normalization test vectors
- Remove SCHEME_PATTERN constant and the no-schema URL prepend logic from RequestValidator; http/https URLs are always expected
- Remove the schema-less test vector ("foo.com/bar") from urlNormalizationVectors
- Add NOSONAR comments on String.format and RuntimeException catch in hmacSha1
Add IPv6, empty path, percent-encoded path vectors from Go SDK.
Remove no-scheme URL support.
@Fivell Fivell force-pushed the test/request-validator-vectors branch from 2486d6a to cd93d57 Compare March 12, 2026 00:43
@Fivell Fivell requested a review from Copilot March 12, 2026 08:07
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

byte[] signatureBytes = hexToBytes(signature);
if (signatureBytes == null) {
return false;
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To fully benefit from a timing-safe comparison and to keep the validator strict, consider rejecting signatures that decode to a different byte length than the computed HMAC (HmacSHA1 is 20 bytes). As written, MessageDigest.isEqual will early-return on length mismatch, and non-standard-length hex signatures (e.g., 2/4/100 hex chars) will be accepted as valid input formats rather than being treated as invalid upfront.

Suggested change
}
}
if (signatureBytes.length != expectedBytes.length) {
return false;
}

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

src/main/java/com/didww/sdk/callback/RequestValidator.java:63

  • normalizeUrl() rebuilds the authority using host directly. For IPv6 literals, URI#getHost() returns the address without brackets (e.g. ::1), so the normalized URL becomes something like http://::1:80/..., which is not a valid URL form and may diverge from other SDKs’ canonicalization. Consider re-wrapping IPv6 hosts in [/] when constructing the normalized URL.
            String path = uri.getRawPath();
            String query = uri.getRawQuery() != null ? "?" + uri.getRawQuery() : "";
            String fragment = uri.getRawFragment() != null ? "#" + uri.getRawFragment() : "";

            return scheme + "://" + userInfo + host + ":" + port + path + query + fragment;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@sonarqubecloud
Copy link

@Fivell Fivell merged commit 85943aa into main Mar 12, 2026
5 checks passed
@Fivell Fivell deleted the test/request-validator-vectors branch March 12, 2026 09:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants